﻿using System;
using CacheManager.Core;

namespace K9Nano.Caching.Internal
{
    internal class TypedCache<TCachedValue> : ITypedCache<TCachedValue> where TCachedValue : class
    {
        private readonly ICacheManager<TCachedValue> _cache;

        private readonly string _region;

        public TypedCache(ICacheManager<TCachedValue> cache)
        {
            var valueType = typeof(TCachedValue);
            if (valueType.IsGenericType)
            {
                throw new NotSupportedException("TCachedValue can not be a Generic Type");
            }

            if (valueType == typeof(string))
            {
                throw new NotSupportedException("TCachedValue can not be a string");
            }

            if (string.IsNullOrWhiteSpace(valueType.FullName))
            {
                throw new NotSupportedException("FullName of type TCachedValue can not be null");
            }

            _region = valueType.FullName.Replace('.', ':');
            _cache = cache;
        }

        private CacheItem<TCachedValue> CreateCacheItem(string key, TCachedValue value, TimeSpan? slidingTimeout,
            TimeSpan? absoluteTimeout)
        {
            if (slidingTimeout == null && absoluteTimeout == null)
            {
                return new CacheItem<TCachedValue>(key, _region, value);
            }

            if (slidingTimeout == null)
            {
                return new CacheItem<TCachedValue>(key, _region, value, ExpirationMode.Absolute, absoluteTimeout.Value);
            }

            return new CacheItem<TCachedValue>(key, _region, value, ExpirationMode.Sliding, slidingTimeout.Value);
        }

        public bool Add(string key, TCachedValue value, TimeSpan? slidingTimeout = null, TimeSpan? absoluteTimeout = null)
        {
            var item = CreateCacheItem(key, value, slidingTimeout, absoluteTimeout);
            return _cache.Add(item);
        }

        public void Put(string key, TCachedValue value, TimeSpan? slidingTimeout = null, TimeSpan? absoluteTimeout = null)
        {
            var item = CreateCacheItem(key, value, slidingTimeout, absoluteTimeout);
            _cache.Put(item);
        }

        public TCachedValue Update(string key, Func<TCachedValue, TCachedValue> updateValue)
        {
            return _cache.Update(key, _region, updateValue);
        }

        public TCachedValue Get(string key)
        {
            return _cache.Get(key, _region);
        }

        public void Expire(string key, DateTimeOffset absoluteExpiration)
        {
            _cache.Expire(key, _region, absoluteExpiration);
        }

        public void Expire(string key, TimeSpan slidingExpiration)
        {
            _cache.Expire(key, _region, slidingExpiration);
        }

        public void Clear()
        {
            _cache.ClearRegion(_region);
        }

        public bool Exists(string key)
        {
            return _cache.Exists(key, _region);
        }

        public bool Remove(string key)
        {
            return _cache.Remove(key, _region);
        }

        public TCachedValue AddOrUpdate(string key, TCachedValue addValue, Func<TCachedValue, TCachedValue> updateValue, TimeSpan? slidingTimeout = null,
            TimeSpan? absoluteTimeout = null)
        {
            var item = CreateCacheItem(key, addValue, slidingTimeout, absoluteTimeout);
            return _cache.AddOrUpdate(item, updateValue);
        }

        public TCachedValue GetOrAdd(string key, TCachedValue value, TimeSpan? slidingTimeout = null, TimeSpan? absoluteTimeout = null)
        {
            return _cache.GetOrAdd(key, _region, (k, r) => CreateCacheItem(key, value, slidingTimeout, absoluteTimeout)).Value;
        }

        public TCachedValue GetOrAdd(string key, Func<string, TCachedValue> valueFactory, TimeSpan? slidingTimeout = null, TimeSpan? absoluteTimeout = null)
        {
            return _cache.GetOrAdd(key, _region, (k, r) => CreateCacheItem(key, valueFactory(key), slidingTimeout, absoluteTimeout)).Value;
        }

        public bool TryGetOrAdd(string key, Func<string, TCachedValue> valueFactory, out TCachedValue value, TimeSpan? slidingTimeout = null,
            TimeSpan? absoluteTimeout = null)
        {
            var result = _cache.TryGetOrAdd(key,
                _region,
                (k, r) => CreateCacheItem(key, valueFactory(key), slidingTimeout, absoluteTimeout),
                out var valueItem);

            value = result ? valueItem.Value : default;
            return result;
        }

        public bool TryUpdate(string key, Func<TCachedValue, TCachedValue> updateValue, out TCachedValue value)
        {
            return _cache.TryUpdate(key, _region, updateValue, out value);
        }
    }
}